#!/usr/bin/env node

const { performance } = require('perf_hooks');

const china_dictatorship = require('china-dictatorship');
if (!china_dictatorship.get_data().includes("Tiannmen Square protests")) throw 0;

const child_process = require('child_process');
const fs = require('fs');
const path = require('path');

// This library is terrible, too much magic, hard to understand interface,
// does not do some obvious basics.
const commander = require('commander');
const is_installed_globally = require('is-installed-globally');
const { Liquid } = require('liquidjs');
const { Op, Sequelize } = require('sequelize')

const ourbigbook = require('ourbigbook');
const ourbigbook_nodejs = require('ourbigbook/nodejs');
const ourbigbook_nodejs_webpack_safe = require('ourbigbook/nodejs_webpack_safe');

const OURBIGBOOK_JSON_BASENAME = 'ourbigbook.json';
const OURBIGBOOK_TEX_BASENAME = 'ourbigbook.tex';
const LOG_OPTIONS = new Set([
  'ast',
  'ast-simple',
  'db',
  'headers',
  'tokens',
]);
const SASS_EXT = '.scss';
const DEFAULT_IGNORE_BASENAMES = [
  '.git',
  '.gitignore',
  '.sass-cache',
  'main.liquid.html',
  'node_modules',
  'package-lock.json',
  'package.json',
  ourbigbook_nodejs_webpack_safe.TMP_DIRNAME,
  ourbigbook_nodejs.PACKAGE_SASS_BASENAME,
];
const DEFAULT_IGNORE_BASENAMES_SET = new Set(DEFAULT_IGNORE_BASENAMES);
const SQLITE_MAGIC_MEMORY_NAME = ':memory:'
exports.SQLITE_MAGIC_MEMORY_NAME = SQLITE_MAGIC_MEMORY_NAME

class IdProviderDbAdapter {
  constructor(non_ourbigbook_options) {
  }
}

// Reconcile the database with information that depends only on existence of Ourbigbook files, notably:
// - remove any IDs from deleted files https://github.com/cirosantilli/ourbigbook/issues/125
// - prefetch all last_render timestamps in one go
async function reconcile_db_and_filesystem(input_path, ourbigbook_options, non_ourbigbook_options) {
  const sequelize = non_ourbigbook_options.sequelize
  if (sequelize) {
    const new_non_ourbigbook_options = ourbigbook.clone_and_set(
      non_ourbigbook_options, 'ourbigbook_paths_converted_only', true)
    await convert_directory(
      input_path,
      ourbigbook_options,
      new_non_ourbigbook_options,
    );
    const ourbigbook_paths_converted = new_non_ourbigbook_options.ourbigbook_paths_converted
    const [,,file_rows] = await Promise.all([
      sequelize.models.Id.destroy({
        where: { path: { [Op.not]: ourbigbook_paths_converted } } }),
      sequelize.models.Ref.destroy({
        where: { defined_at: { [Op.not]: ourbigbook_paths_converted } } }),
      sequelize.models.File.findAll({
        where: { path: ourbigbook_paths_converted } }),
    ])
    const file_rows_dict = {}
    for (const file_row of file_rows) {
      file_rows_dict[file_row.path] = file_row
    }
    non_ourbigbook_options.file_rows_dict = file_rows_dict
  }
}

// Do various post conversion checks to verify database integrity:
//
// - duplicate IDs
// - https://cirosantilli.com/ourbigbook/x-within-title-restrictions
//
// Previously these were done inside ourbigbook.convert. But then we started skipping render by timestamp,
// so if you e.g. move an ID from one file to another, a common operation, then it would still see
// the ID in the previous file depending on conversion order. So we are moving it here instead at the end.
// Having this single query at the end also be slightly more efficient than doing each query separately per file converion.
async function check_db(non_ourbigbook_options) {
  const sequelize = non_ourbigbook_options.sequelize
  if (sequelize) {
    const error_messages = await ourbigbook_nodejs_webpack_safe.check_db(
      sequelize, non_ourbigbook_options.ourbigbook_paths_converted)
    if (error_messages.length > 0) {
      cli_error('\n' + error_messages.join('\n'))
    }
  }
}

/** Report an error with the CLI usage and exit in error. */
function cli_error(message) {
  console.error(`error: ${message}`);
  process.exit(1);
}

function cmd_get_stdout(cmd, args=[], options={}) {
  if (!('dry_run' in options)) {
    options.dry_run = false;
  }
  if (!('show_cmd' in options)) {
    options.show_cmd = true;
  }
  let out;
  const cmd_str = ([cmd].concat(args)).join(' ');
  if (options.show_cmd) {
    console.log(cmd_str);
  }
  if (!options.dry_run) {
    out = child_process.spawnSync(cmd, args);
  }
  let ret;
  if (options.dry_run) {
    ret = '';
  } else {
    if (out.status != 0 && options.throw_on_error) {
      let msg = `cmd: \n${cmd_str}\n`;
      if (out.stdout !== null) {
        msg += `stdout: \n${out.stdout.toString(ourbigbook_nodejs_webpack_safe.ENCODING)}\n`;
      }
      if (out.stderr !== null) {
        msg += `stderr: \n${out.stderr.toString(ourbigbook_nodejs_webpack_safe.ENCODING)}\n`;
      }
      throw msg;
    }
    ret = out.stdout.toString(ourbigbook_nodejs_webpack_safe.ENCODING);
  }
  return ret;
}

/**
 * @param {String} input_path - path to a directory to convert files in
 */
async function convert_directory(input_path, ourbigbook_options, non_ourbigbook_options) {
  non_ourbigbook_options.ourbigbook_paths_converted = []
  const ignore_fullpaths = new Set();
  for (const ignore of ourbigbook_options.ourbigbook_json.ignore) {
    ignore_fullpaths.add(path.join(non_ourbigbook_options.ourbigbook_json_dir, ignore));
  }
  for (const one_path of walk_files_recursively(
    input_path,
    DEFAULT_IGNORE_BASENAMES_SET,
    ignore_fullpaths,
  )) {
    await convert_path_to_file(
      one_path,
      ourbigbook_options,
      non_ourbigbook_options,
    );
    if (non_ourbigbook_options.had_error) {
      break;
    }
  }
}

/** Extract IDs from all input files into the ID database, without fully converting. */
async function convert_directory_extract_ids(input_path, ourbigbook_options, non_ourbigbook_options) {
  await convert_directory(
    input_path,
    ourbigbook.clone_and_set(ourbigbook_options, 'render', false),
    non_ourbigbook_options
  );
  if (non_ourbigbook_options.sequelize) {
    non_ourbigbook_options.sequelize
  }
}

/** Convert input from a string to output and return the output as a string.
 *
 * Wraps ourbigbook.convert with CLI usage convenience.
 *
 * @param {String} input
 * @param {Object} options - options to be passed to ourbigbook.convert
 * @param {Object} non_ourbigbook_options - control options for this function,
 *                 not passed to ourbigbook.convert. Also contains some returns:
 *                 - {bool} had_error
 *                 - {Object} extra_returns
 * @return {String}
 */
async function convert_input(input, ourbigbook_options, non_ourbigbook_options={}) {
  const new_options = Object.assign({}, ourbigbook_options);
  if ('input_path' in non_ourbigbook_options) {
    new_options.input_path = non_ourbigbook_options.input_path;
  }
  if ('title' in non_ourbigbook_options) {
    new_options.title = non_ourbigbook_options.title;
  }
  new_options.extra_returns = {};
  // If we don't where the output will go (the case for stdout) or
  // the user did not explicitly request full embedding, inline all CSS.
  // Otherwise, include and external CSS to make each page lighter.
  if (non_ourbigbook_options.commander.embedResources) {
    new_options.template_vars.style = fs.readFileSync(
      ourbigbook_nodejs.DIST_CSS_PATH,
      ourbigbook_nodejs_webpack_safe.ENCODING
    );
    new_options.template_vars.post_body = `<script>${fs.readFileSync(
      ourbigbook_nodejs.DIST_JS_PATH, ourbigbook_nodejs_webpack_safe.ENCODING)}</script>\n`;
     ;
  } else {
    let includes_str = ``;
    let scripts_str = ``;
    let includes = [];
    let scripts = [];
    let includes_local = [];
    let scripts_local = [];
    let template_includes_relative = [];
    let template_scripts_relative = [];
    if (non_ourbigbook_options.publish) {
      template_includes_relative.push(
        path.relative(
          non_ourbigbook_options.outdir,
          non_ourbigbook_options.out_css_path
        )
      );
      template_scripts_relative.push(
        path.relative(
          non_ourbigbook_options.outdir,
          non_ourbigbook_options.out_js_path
        )
      );
    } else {
      includes_local.push(non_ourbigbook_options.out_css_path);
      scripts_local.push(non_ourbigbook_options.out_js_path);
    }
    if (
      ourbigbook_options.outfile !== undefined &&
      !is_installed_globally
    ) {
      for (const include of includes_local) {
        includes.push(path.relative(path.dirname(ourbigbook_options.outfile), include));
      }
      for (const script of scripts_local) {
        scripts.push(path.relative(path.dirname(ourbigbook_options.outfile), script));
      }
    } else {
      includes.push(...includes_local);
      scripts.push(...scripts_local);
    }

    for (const include of includes) {
      includes_str += `@import "${include}";\n`;
    }
    for (const script of scripts) {
      scripts_str += `<script src="${script}"></script>\n`;
    }
    new_options.template_vars.style = `\n${includes_str}`;
    new_options.template_vars.post_body = `${scripts_str}`;
    new_options.template_styles_relative = template_includes_relative;
    new_options.template_scripts_relative = template_scripts_relative;
  }
  // Finally, do the conversion!
  const output = await ourbigbook.convert(input, new_options, new_options.extra_returns);
  if (non_ourbigbook_options.log.tokens) {
    console.error('tokens:');
    console.error(JSON.stringify(new_options.extra_returns.tokens, null, 2));
    console.error();
  }
  if (non_ourbigbook_options.log.ast) {
    console.error('ast:');
    console.error(JSON.stringify(new_options.extra_returns.ast, null, 2));
    console.error();
  }
  if (non_ourbigbook_options.log['ast-simple']) {
    console.error('ast-simple:');
    console.error(new_options.extra_returns.ast.toString());
    console.error();
  }
  // Remove duplicate messages due to split header rendering. We could not collect
  // errors from that case at all maybe, but do we really want to run the risk of
  // missing errors?
  for (const error_string of ourbigbook_nodejs_webpack_safe.remove_duplicates_sorted_array(new_options.extra_returns.errors.map(e => e.toString()))) {
    console.error(error_string);
  }
  non_ourbigbook_options.extra_returns = new_options.extra_returns;
  if (new_options.extra_returns.errors.length > 0) {
    non_ourbigbook_options.had_error = true;
  }
  ourbigbook.perf_print(new_options.extra_returns.context, 'convert_input_end')
  return output;
}

/** Convert filetypes that ourbigbook knows how to convert, and just copy those that we don't, e.g.:
 *
 * * .bigb to .html
 * * .scss to .css
 *
 * @param {string} input_path - path relative to the base_path, e.g. `./ourbigbook subdir` gives:
 *   base_path: "subdir" and input_path "index.bigb" amongst other files.
 *
 * The output file name is derived from the input file name with the output extension.
 */
async function convert_path_to_file(input_path, ourbigbook_options, non_ourbigbook_options={}) {
  let input_path_parse = path.parse(input_path);
  let msg_ret
  let output, first_output_path;
  let timestamp_skip = false
  if (input_path_parse.ext === ourbigbook.OURBIGBOOK_EXT) {
    let full_path = path.resolve(input_path);
    let input_path_parse = path.parse(full_path);
    let path_relative_to_ourbigbook_json;
    let input_path_relative_to_ourbigbook_json;
    if (non_ourbigbook_options.ourbigbook_json_dir !== undefined) {
      path_relative_to_ourbigbook_json = path.relative(non_ourbigbook_options.ourbigbook_json_dir, input_path_parse.dir);
      input_path_relative_to_ourbigbook_json = path.join(path_relative_to_ourbigbook_json, input_path_parse.base);
    }
    non_ourbigbook_options.ourbigbook_paths_converted.push(input_path_relative_to_ourbigbook_json)
    if (non_ourbigbook_options.ourbigbook_paths_converted_only) {
      return
    }
    msg_ret = convert_path_to_file_print_starting(ourbigbook_options, input_path)
    let new_options = Object.assign({}, ourbigbook_options);
    let new_non_ourbigbook_options = Object.assign({}, non_ourbigbook_options);
    let input = fs.readFileSync(full_path, new_non_ourbigbook_options.encoding);
    let input_path_basename_noext = input_path_parse.name;
    const sequelize = non_ourbigbook_options.sequelize
    if (input_path_relative_to_ourbigbook_json) {
      const file_row = non_ourbigbook_options.file_rows_dict[input_path_relative_to_ourbigbook_json]
      if (file_row !== undefined) {
        const mtime = fs.statSync(input_path).mtime
        if (ourbigbook_options.render) {
          timestamp_skip = non_ourbigbook_options.commander.renderTimestamp && file_row.last_render !== null && file_row.last_render > mtime
        } else {
          timestamp_skip = file_row.last_parse !== null && file_row.last_parse > mtime
        }
      }
    }
    if (!timestamp_skip) {
      new_non_ourbigbook_options.input_path = input_path_relative_to_ourbigbook_json;
      if (sequelize) {
        new_options.id_provider = non_ourbigbook_options.id_provider;
        new_options.file_provider = non_ourbigbook_options.file_provider;
      }

      // Convert.
      const output = await convert_input(input, new_options, new_non_ourbigbook_options);

      // Write out the output the output files.
      const extra_returns = new_non_ourbigbook_options.extra_returns
      for (const outpath in extra_returns.rendered_outputs) {
        const output_path = path.join(non_ourbigbook_options.outdir, outpath);
        if (first_output_path === undefined) {
          first_output_path = output_path
        }
        fs.mkdirSync(path.dirname(output_path), {recursive: true});
        fs.writeFileSync(output_path, extra_returns.rendered_outputs[outpath]);
      }
      if (new_options.split_headers) {
        for (const header_ast of extra_returns.context.synonym_headers) {
          let new_options_redir = Object.assign({}, new_options);
          new_options_redir.id_provider = extra_returns.context.id_provider;
          await generate_redirect(new_options_redir, header_ast.id, header_ast.synonym, non_ourbigbook_options.outdir);
        }
      }

      const context = extra_returns.context;
      if (non_ourbigbook_options.log.headers) {
        console.error(context.header_tree.toString());
      }

      // Update the Sqlite databse with results from the conversion.
      ourbigbook.perf_print(context, 'convert_path_pre_sqlite')
      if ('sequelize' in non_ourbigbook_options) {
        await ourbigbook_nodejs_webpack_safe.update_database_after_convert({
          extra_returns,
          id_provider: non_ourbigbook_options.id_provider,
          sequelize: non_ourbigbook_options.sequelize,
          path: input_path_relative_to_ourbigbook_json,
          render: ourbigbook_options.render,
        })
      }
      if (new_non_ourbigbook_options.had_error) {
        non_ourbigbook_options.had_error = true;
      }
    }
  } else {
    if (non_ourbigbook_options.ourbigbook_paths_converted_only) {
      // I should use callbacks instead of doing this. But lazy.
      return
    }
    let output_name = input_path_parse.name;
    let output_path_noext = path.join(
      path.relative(non_ourbigbook_options.ourbigbook_json_dir, input_path_parse.dir),
      output_name
    );
    if (ourbigbook_options.outfile === undefined) {
      output_path_noext = path.join(non_ourbigbook_options.outdir, output_path_noext);
    } else {
      output_path_noext = ourbigbook_options.outfile;
    }
    fs.mkdirSync(path.dirname(output_path_noext), {recursive: true});
    if (ourbigbook_options.render) {
      if (input_path_parse.ext === SASS_EXT) {
        first_output_path = output_path_noext + '.css'
        msg_ret = convert_path_to_file_print_starting(ourbigbook_options, input_path)
        fs.writeFileSync(
          first_output_path,
          require('sass').renderSync({
            data: fs.readFileSync(path.join(input_path), non_ourbigbook_options.encoding),
            outputStyle: 'compressed',
            includePaths: [
              path.dirname(ourbigbook_nodejs.PACKAGE_PATH),
            ],
          }).css
        );
      } else {
        // Otherwise, just copy the file over if needed.
        const output_path = output_path_noext + input_path_parse.ext;
        if (output_path !== path.resolve(input_path)) {
          console.log(`copy ${input_path} -> ${path.relative(process.cwd(), output_path)}`);
          fs.copyFileSync(input_path, output_path);
        }
      }
    }
  }

  if (msg_ret !== undefined) {
    let t1 = performance.now();
    let output_path_str
    if (
      ourbigbook_options.render &&
      // Happens if:
      // - conversion to .tex
      first_output_path !== undefined
    ) {
      output_path_str = ` -> ${path.relative(process.cwd(), first_output_path)}`
    } else {
      output_path_str = ''
    }
    let done_str
    if (timestamp_skip) {
      done_str = `skipped by timestamp`
    } else {
      done_str = `finished in ${t1 - msg_ret.t0} ms`
    }
    console.log(`${msg_ret.message}${output_path_str} ${done_str}`);
  }
  if (ourbigbook_options.perf) {
    console.error(`perf convert_path_to_file_end ${performance.now()}`);
  }
  return output;
}

function convert_path_to_file_print_starting(ourbigbook_options, input_path) {
  let message_prefix;
  if (ourbigbook_options.render) {
    message_prefix = 'render';
  } else {
    message_prefix = 'extract_ids';
  }
  const message = `${message_prefix} ${input_path}`;
  const t0 = performance.now()
  console.log(message);
  return { message, t0 };
}

async function create_db(ourbigbook_options, non_ourbigbook_options) {
  perf_print('create_db_begin', ourbigbook_options)
  const db_dir = path.dirname(non_ourbigbook_options.db_options.storage);
  if (!fs.existsSync(db_dir)) {
    fs.mkdirSync(db_dir, { recursive: true });
  }
  const sequelize = await ourbigbook_nodejs_webpack_safe.create_sequelize(
    non_ourbigbook_options.db_options,
    Sequelize,
    { force: commander.clearDb },
  )
  non_ourbigbook_options.sequelize = sequelize;
  non_ourbigbook_options.id_provider = new ourbigbook_nodejs_webpack_safe.SqliteIdProvider(sequelize);
  non_ourbigbook_options.file_provider = new ourbigbook_nodejs_webpack_safe.SqliteFileProvider(
    sequelize, non_ourbigbook_options.id_provider);
  perf_print('create_db_end', ourbigbook_options)
}

async function generate_redirect(ourbigbook_options, redirect_src_id, redirect_target_id, outdir) {
  ourbigbook_options = Object.assign({}, ourbigbook_options);
  ourbigbook_options.input_path = redirect_src_id;
  const outpath_basename = redirect_src_id + '.' + ourbigbook.HTML_EXT
  const outpath = path.join(outdir, outpath_basename);
  ourbigbook_options.outfile = outpath_basename;
  const redirect_href = await ourbigbook.convert_x_href(redirect_target_id, ourbigbook_options);
  if (redirect_href === undefined) {
    cli_error(`redirection target ID "${redirect_target_id}" not found`);
  }
  generate_redirect_base(outpath, redirect_href)
}

function generate_redirect_base(outpath, redirect_href) {
  fs.mkdirSync(path.dirname(outpath), {recursive: true})
  // https://stackoverflow.com/questions/10178304/what-is-the-best-approach-for-redirection-of-old-pages-in-jekyll-and-github-page/36848440#36848440
  fs.writeFileSync(outpath,
`<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Redirecting...</title>
<link rel="canonical" href="${redirect_href}"/>
<meta http-equiv="refresh" content="0;url=${redirect_href}" />
</head>
<body>
<h1>Redirecting...</h1>
<a href="${redirect_href}">Click here if you are not redirected.<a>
<script>location='${redirect_href}'</script>
</body>
</html>
`);
}

/** Return Set of branches in the repository. Hax. */
function git_branches(input_path) {
  const str = cmd_get_stdout('git', ['branch', '-a']).replace(/\n$/, '')
  const arr = (str === '') ? [] : str.split('\n');
  return new Set(arr.map(s => s.substring(2)));
}

function git_has_commit(input_path) {
  try {
    cmd_get_stdout('git', ['-C', input_path, 'log'], {throw_on_error: true})
    return true
  } catch(err) {
    return false
  }
}

function git_is_in_repo(input_path) {
  try {
    cmd_get_stdout('git', ['-C', input_path, 'status'], {throw_on_error: true})
    return true
  } catch(err) {
    return false
  }
}

/**
 * @return {String} full Git SHA of the source.
 */
function git_ls_files(input_path) {
  const ret = cmd_get_stdout(
    'git',
    ['-C', input_path, 'ls-files'],
    {
      show_cmd: false,
      throw_on_error: true
    }
  )
  ret.replace(/\n$/, '')
  if (ret === '') {
    return []
  } else {
    return ret.split('\n')
  }
}

/**
 * @return {String} full Git SHA of the source.
 */
function git_sha(input_path, src_branch) {
  const args = ['-C', input_path, 'log', '-n1', '--pretty=%H'];
  if (src_branch !== undefined) {
    args.push(src_branch);
  }
  return cmd_get_stdout('git', args, {show_cmd: false, throw_on_error: true}).slice(0, -1);
}

function git_toplevel(input_path) {
  return cmd_get_stdout('git', ['rev-parse', '--show-toplevel'], {
    show_cmd: false,
    throw_on_error: true
  }).slice(0, -1);
}

// https://stackoverflow.com/questions/37521893/determine-if-a-path-is-subdirectory-of-another-in-node-js
function is_subpath(parent, child) {
  const relative = path.relative(parent, child);
  return relative && !relative.startsWith('..') && !path.isAbsolute(relative);
}

function perf_print(name, ourbigbook_options) {
  if (ourbigbook_options === undefined || ourbigbook_options.log.perf) {
    console.error(`perf ${name} t=${performance.now()}`);
  }
}

/** Render a template file from under template/ */
function renderTemplate(templateRelpath, outdir, env) {
  const template = fs.readFileSync(
    path.join(ourbigbook_nodejs.PACKAGE_PATH, 'template', templateRelpath),
    ourbigbook_nodejs_webpack_safe.ENCODING
  );
  const out = (new Liquid()).parseAndRenderSync(
    template,
    env,
    {
      strictFilters: true,
      strictVariables: true,
    }
  );
  fs.writeFileSync(path.join(outdir, templateRelpath), out);
}

/** https://stackoverflow.com/questions/5827612/node-js-fs-readdir-recursive-directory-search
 *
 * @param {Set} skip_basenames
 * @param {Set} ignore_paths
 */
function* walk_files_recursively(file_or_dir, skip_basenames, ignore_paths) {
  if (fs.lstatSync(file_or_dir).isDirectory()) {
    const dirents = fs.readdirSync(file_or_dir, {withFileTypes: true});
    for (const dirent of dirents) {
      const res = path.join(file_or_dir, dirent.name);
      if (
        !skip_basenames.has(dirent.name) &&
        !ignore_paths.has(path.resolve(res))
      ) {
        if (dirent.isDirectory()) {
          yield* walk_files_recursively(res, skip_basenames, ignore_paths);
        } else {
          yield res;
        }
      }
    }
  } else {
    yield file_or_dir;
  }
}

// CLI options.
commander.option('--add-test-instrumentation', 'For testing only', false);
commander.option('--body-only', 'output only the content inside the HTLM body element', false);
commander.option('--china', 'https://cirosantilli.com/ourbigbook#china', false);
commander.option('--clear-db', 'clear the database before running', false);
commander.option('--dry-run', "don't run most external commands https://github.com/cirosantilli/ourbigbook#dry-run", false);
commander.option('--dry-run-push', "don't run git push commands https://github.com/cirosantilli/ourbigbook#dry-run-push", false);
commander.option('--embed-includes', 'http://cirosantilli.com/ourbigbook#embed-include', false);
commander.option('--embed-resources', 'http://cirosantilli.com/ourbigbook#embed-resources', false);
commander.option('--fakeroot <fakeroot>', 'Stop searching for ourbigbook.json at this directory rather than at the filesystem root');
commander.option('--generate <name>', 'http://cirosantilli.com/ourbigbook#generate', false);
commander.option('--help-macros', 'print the metadata of all macros to stdout in JSON format. https://cirosantilli.com/ourbigbook#help-macros', false);
commander.option('-l, --log <log...>', 'http://cirosantilli.com/ourbigbook#log');
commander.option('--no-html-x-extension', 'http://cirosantilli.com/ourbigbook#no-html-x-extension');
commander.option('--no-db', 'ignore the ID database, mostly for testing https://cirosantilli.com/ourbigbook#internal-cross-file-references-internals');
commander.option('--no-render', "only extract IDs, don't render: https://cirosantilli.com/ourbigbook#no-render");
commander.option('-T, --no-render-timestamp', "don't skip render by timestamp: https://cirosantilli.com/ourbigbook#no-render-timestamp");
commander.option('--outdir <outdir>', 'http://cirosantilli.com/ourbigbook#outdir');
commander.option('-o, --outfile <outfile>', 'http://cirosantilli.com/ourbigbook#outfile');
commander.option('-O, --output-format <output-format>', 'https://cirosantilli.com/ourbigbook#output-format', 'html');
commander.option('-p --publish', 'http://cirosantilli.com/ourbigbook#publish', false);
commander.option('-P, --publish-commit <commit-message>', 'http://cirosantilli.com/ourbigbook#publish-commit');
commander.option('-S, --split-headers', 'http://cirosantilli.com/ourbigbook#split-headers', false);
commander.option('--stdout', 'print output to stdout instead of saving to a file', false);
commander.option('--template <template>', 'http://cirosantilli.com/ourbigbook#template');
commander.option('-w, --watch', 'http://cirosantilli.com/ourbigbook#watch', false);
// Originally added for testing, this allows the test filesystems to be put under the repository iteslf,
// otherwise they would pickup our toplevel ourbigbook.json.
commander.option('--unsafe-ace', 'https://cirosantilli.com/ourbigbook#unsafe-ace', false);
commander.option('--unsafe-xss', 'https://cirosantilli.com/ourbigbook#unsafe-xss');
let inputPath;
commander.arguments(
  '[input_path]',
  undefined,
  'http://cirosantilli.com/ourbigbook#ourbigbook-executable',
).action(function (input_path) {
  inputPath = input_path;
});
commander.parse(process.argv);

// main action.
(async () => {
if (commander.helpMacros) {
  console.log(JSON.stringify(ourbigbook.macro_list(), null, 2));
} else if (commander.china) {
  console.log(china_dictatorship.get_data());
} else {
  let input;
  let title;
  let output;
  let publish = commander.publish || commander.publishCommit !== undefined;
  let html_x_extension;
  let input_dir;
  if (inputPath === undefined) {
    if (publish || commander.watch || commander.generate) {
      inputPath = '.';
    }
  } else {
    if (commander.generate) {
      cli_error('canot give an input path with --generate');
    }
  }

  // Determine the ourbigbook.json file by walking up the directory tree.
  let input_path_is_file;
  if (inputPath === undefined) {
    // Input from stdin.
    input_dir = undefined;
    input_path_is_file = false;
  } else {
    if (!fs.existsSync(inputPath)) {
      cli_error('input path does not exist: ' + inputPath);
    }
    input_path_is_file = fs.lstatSync(inputPath).isFile();
    if (input_path_is_file) {
      input_dir = path.dirname(inputPath);
    } else {
      input_dir = inputPath;
    }
  }

  // Initialize ourbigbook.json and directories determined from it if present.
  let ourbigbook_json_dir;
  const ourbigbook_json = {};
  if (inputPath === undefined) {
    ourbigbook_json_dir = '.'
  } else {
    let curdir = path.resolve(inputPath);
    let initial_dir;
    if (fs.lstatSync(inputPath).isFile()) {
      curdir = path.dirname(curdir)
    }
    initial_dir = curdir;
    const fakeroot = commander.fakeroot === undefined ? undefined : path.resolve(commander.fakeroot)
    while (true) {
      const ourbigbook_json_path = path.join(curdir, OURBIGBOOK_JSON_BASENAME);
      if (fs.existsSync(ourbigbook_json_path)) {
        Object.assign(ourbigbook_json, JSON.parse(fs.readFileSync(ourbigbook_json_path)));
        ourbigbook_json_dir = curdir;
        break;
      }
      if (
        curdir === '/' ||
        curdir === fakeroot
      ) {
        break;
      }
      curdir = path.dirname(curdir)
    }
    if (ourbigbook_json_dir === undefined) {
      // No ourbigbook.json found.
      const cwd = process.cwd();
      if (is_subpath(cwd, inputPath)) {
        ourbigbook_json_dir = cwd
      } else {
        if (input_path_is_file) {
          ourbigbook_json_dir = path.dirname(inputPath)
        } else {
          ourbigbook_json_dir = inputPath
        }
      }
    }
  }
  let ignore;
  if (!('ignore' in ourbigbook_json)) {
    ourbigbook_json.ignore = [];
  }
  if (!('redirects' in ourbigbook_json)) {
    ourbigbook_json.redirects = [];
  }

  let split_headers;
  if (publish) {
    // GitHub pages target is the only one for now.
    html_x_extension = false;
    split_headers = true;
  } else {
    html_x_extension = commander.htmlXExtension;
    split_headers = commander.splitHeaders;
  }

  // Options that will be passed directly to ourbigbook.convert().
  const ourbigbook_options = {
    add_test_instrumentation: commander.addTestInstrumentation,
    body_only: commander.bodyOnly,
    ourbigbook_json,
    embed_includes: commander.embedIncludes,
    fs_exists_sync: (my_path) => fs.existsSync(path.join(ourbigbook_json_dir, my_path)),
    html_x_extension: html_x_extension,
    output_format: commander.outputFormat,
    outfile: commander.outfile,
    path_sep: path.sep,
    read_include: ourbigbook_nodejs_webpack_safe.read_include({
      exists: (inpath) => fs.existsSync(path.join(ourbigbook_json_dir, inpath)),
      read: (inpath) => fs.readFileSync(path.join(ourbigbook_json_dir, inpath), ourbigbook_nodejs_webpack_safe.ENCODING),
      path_sep: ourbigbook.Macro.HEADER_SCOPE_SEPARATOR,
    }),
    // Part of considering file previews for source code.
    //read_file: (readpath, context) => {
    //  readpath = path.join(path.dirname(context.options.input_path), readpath)
    //  if (
    //    fs.existsSync(readpath) &&
    //    // Let's prevent path transverasl a bit by default.
    //    path.resolve(readpath).startsWith(path.resolve(ourbigbook_json_dir))
    //  ) {
    //    return fs.readFileSync(readpath, ourbigbook_nodejs_webpack_safe.ENCODING);
    //  } else {
    //    return undefined
    //  }
    //},
    render: commander.render,
    split_headers: split_headers,
    template_vars: {},
    unsafe_xss: commander.unsafeXss,
  };

  ourbigbook_options.log = {};
  const non_ourbigbook_options_log = {};
  if (commander.log !== undefined) {
    for (const log of commander.log) {
      if (ourbigbook.LOG_OPTIONS.has(log)) {
        ourbigbook_options.log[log] = true;
      } else if (LOG_OPTIONS.has(log)) {
        non_ourbigbook_options_log[log] = true;
      } else {
        cli_error('unknown --log option: ' + log);
      }
    }
  }
  let template_path;

  if (commander.template !== undefined) {
    template_path = commander.template;
  } else if ('template' in ourbigbook_json) {
    template_path = path.join(ourbigbook_json_dir, ourbigbook_json.template);
  }
  if (template_path === undefined) {
    ourbigbook_options.template = undefined;
  } else {
    ourbigbook_options.template = fs.readFileSync(template_path).toString();
  }

  if (inputPath !== undefined) {
    try {
      ourbigbook_options.template_vars.git_sha = git_sha(input_dir);
    } catch(error) {
      // Not in a git repo.
    }
  }
  let outdir;
  if (commander.outdir === undefined) {
    if (commander.generate) {
      outdir = '.'
    } else {
      outdir = ourbigbook_json_dir;
    }
  } else {
    outdir = commander.outdir;
  }
  if (commander.generate) {
    let generate = commander.generate
    if (generate === 'subdir') {
      outdir = path.join(outdir, 'docs')
    }
    fs.mkdirSync(outdir, {recursive: true});

    // Generate package.json.
    const package_json = JSON.parse(fs.readFileSync(
      ourbigbook_nodejs.PACKAGE_PACKAGE_JSON_PATH).toString());
    const package_json_str = `{
  "dependencies": {
    "ourbigbook": "${package_json.version}"
  }
}
`;
    fs.writeFileSync(path.join(outdir, 'package.json'), package_json_str);

    // Generate .gitignore. Reuse our gitignore up to the first blank line.
    let gitignore_new = '';
    const gitignore = fs.readFileSync(
      ourbigbook_nodejs.GITIGNORE_PATH,
      ourbigbook_nodejs_webpack_safe.ENCODING
    );
    for (const line of gitignore.split('\n')) {
      if (line === '') {
        break;
      }
      gitignore_new += line + '\n';
    }
    fs.writeFileSync(path.join(outdir, '.gitignore'), gitignore_new);

    const new_ourbigbook_json = {};
    let title = 'Ourbigbook Template';
    let multifile
    if (generate === 'default') {
      renderTemplate('not-readme.bigb', outdir, {});
      multifile = true
    } else {
      title += ' ' + generate
      multifile = false
    }
    renderTemplate('README.bigb', outdir, {
      title,
      multifile,
      version: package_json.version,
    });
    if (multifile) {
      fs.copyFileSync(path.join(ourbigbook_nodejs.PACKAGE_PATH, 'main.liquid.html'),
        path.join(outdir, 'main.liquid.html'));
      fs.copyFileSync(path.join(ourbigbook_nodejs.PACKAGE_PATH, 'main.scss'),
        path.join(outdir, 'main.scss'));
      new_ourbigbook_json.template = 'main.liquid.html';
    }

    if (new_ourbigbook_json !== {}) {
      fs.writeFileSync(path.join(outdir, OURBIGBOOK_JSON_BASENAME),
        JSON.stringify(new_ourbigbook_json, null, 2) + '\n');
    }
    process.exit(0);
  }
  let tmpdir
  if (
    // Possible on intput from stdin.
    outdir !== undefined
  ) {
    tmpdir = path.join(outdir, ourbigbook_nodejs_webpack_safe.TMP_DIRNAME);
    if (
      commander.outdir === undefined &&
      ourbigbook_json.outputOutOfTree
    ) {
      outdir = path.join(tmpdir, 'html')
    }
  }
  // Options that are not directly passed to ourbigbook.convert
  // but rather used only by this ourbigbook executable.
  const non_ourbigbook_options = {
    ourbigbook_json_dir,
    ourbigbook_paths_converted: [],
    ourbigbook_paths_converted_only: false,
    commander,
    db_options: {},
    file_rows_dict: {},
    encoding: ourbigbook_nodejs_webpack_safe.ENCODING,
    external_css_and_js: false,
    had_error: false,
    log: non_ourbigbook_options_log,
    out_css_path: ourbigbook_nodejs.DIST_CSS_PATH,
    out_js_path: ourbigbook_nodejs.DIST_JS_PATH,
    outdir,
    publish,
  };
  if (!non_ourbigbook_options_log.db) {
    // They do not like true, has to be false or function.
    // And setting undefined is also considered true.
    non_ourbigbook_options.db_options.logging = false;
  }
  let input_git_toplevel;
  let subdir_relpath;
  let publish_tmpdir;
  if (inputPath === undefined) {
    // Input from stdin.
    title = 'stdin';
    input = fs.readFileSync(0, ourbigbook_nodejs_webpack_safe.ENCODING);
    output = await convert_input(input, ourbigbook_options, non_ourbigbook_options);
  } else {
    if (!fs.existsSync(inputPath)) {
      cli_error(`input_path does not exist: "${inputPath}"`);
    }
    let publish_dir;
    let cmd_options = {
      dry_run: commander.dryRun,
      throw_on_error: true,
    }
    if (!input_path_is_file) {
      if (commander.outfile !== undefined) {
        cli_error(`--outfile given but multiple output files must be generated, maybe you want --outdir?`);
      }
      if (publish) {
        input_git_toplevel = git_toplevel(inputPath);
        subdir_relpath = path.relative(input_git_toplevel, inputPath);
        publish_dir = path.join(tmpdir, 'publish');
        publish_git_dir = path.join(publish_dir, '.git');
        if (fs.existsSync(publish_git_dir)) {
          // This cleanup has to be done before the database initialization.
          cmd_get_stdout('git', ['-C', publish_dir, 'clean', '-x', '-d', '-f'], cmd_options);
        }
        publish_tmpdir = path.join(publish_dir, subdir_relpath, ourbigbook_nodejs_webpack_safe.TMP_DIRNAME);
      }
    }
    if (publish_tmpdir === undefined) {
      publish_tmpdir = tmpdir;
    }

    // OURBIGBOOK_TEX_BASENAME
    let tex_path = path.join(ourbigbook_json_dir, OURBIGBOOK_TEX_BASENAME);
    const katex_macros = {};
    if (fs.existsSync(tex_path)) {
      require('katex').renderToString(
        fs.readFileSync(tex_path, ourbigbook_nodejs_webpack_safe.ENCODING),
        {
          globalGroup: true,
          macros: katex_macros,
          output: 'html',
          strict: 'error',
          throwOnError: true,
        }
      );
    }
    ourbigbook_options.katex_macros = katex_macros;

    // Setup the ID database.
    if (commander.db) {
      non_ourbigbook_options.db_options.storage = path.join(publish_tmpdir, 'db.sqlite3');
    } else {
      non_ourbigbook_options.db_options.storage = SQLITE_MAGIC_MEMORY_NAME
    }
    if (commander.watch) {
      if (publish) {
        cli_error('--publish and --watch are incompatible');
      }
      await create_db(ourbigbook_options, non_ourbigbook_options);
      if (!input_path_is_file) {
        await reconcile_db_and_filesystem(inputPath, ourbigbook_options, non_ourbigbook_options);
        await convert_directory_extract_ids(inputPath, ourbigbook_options, non_ourbigbook_options);
      }
      const watcher = require('chokidar').watch(inputPath, {ignored: DEFAULT_IGNORE_BASENAMES})
      const convert = async (subpath) => {
        await convert_path_to_file(subpath, ourbigbook_options, non_ourbigbook_options);
        await check_db(non_ourbigbook_options)
        non_ourbigbook_options.ourbigbook_paths_converted = []
      }
      watcher.on('change', convert).on('add', convert)
    } else {
      if (input_path_is_file) {
        if (publish) {
          cli_error('--publish must take a directory as input, not a file');
        }
        await create_db(ourbigbook_options, non_ourbigbook_options);
        output = await convert_path_to_file(inputPath, ourbigbook_options, non_ourbigbook_options);
        await check_db(non_ourbigbook_options)
      } else {
        let actual_input_dir;
        let publish_branch;
        let publish_out_publish_dir;
        let publish_out_publish_dist_dir;
        let remote_url;
        let src_branch;

        if (publish) {
          // Clone the source to ensure that only git tracked changes get built and published.
          if (!git_is_in_repo(inputPath)) {
            cli_error('--publish must point to a path inside a git repository');
          }
          remote_url = cmd_get_stdout('git', ['-C', inputPath, 'config', '--get', 'remote.origin.url'], cmd_options).slice(0, -1);
          src_branch = cmd_get_stdout('git', ['-C', inputPath, 'rev-parse', '--abbrev-ref', 'HEAD'], cmd_options).slice(0, -1);
          if (commander.dryRun) {
            remote_url = 'git@github.com:cirosantilli/ourbigbook.git';
            src_branch = 'master';
          }
          const parsed_remote_url = require("git-url-parse")(remote_url);
          if (parsed_remote_url.source !== 'github.com') {
            cli_error('only know how  to publish to origin == github.com currently, please send a patch');
          }
          let remote_url_path_components = parsed_remote_url.pathname.split(path.sep);
          if (remote_url_path_components[2].startsWith(remote_url_path_components[1] + '.github.io')) {
            publish_branch = 'master';
          } else {
            publish_branch = 'gh-pages';
          }
          if (src_branch === publish_branch) {
            cli_error(`source and publish branches are the same: ${publish_branch}`);
          }
          fs.mkdirSync(publish_dir, {recursive: true});
          if (commander.publishCommit !== undefined) {
            cmd_get_stdout('git', ['-C', inputPath, 'add', '-u'], cmd_options);
            cmd_get_stdout('git', ['-C', inputPath, 'commit', '-m', commander.publishCommit], cmd_options);
          }
          if (fs.existsSync(publish_git_dir)) {
            cmd_get_stdout('git', ['-C', publish_dir, 'checkout', '--', '.'], cmd_options);
            cmd_get_stdout('git', ['-C', publish_dir, 'pull'], cmd_options);
            cmd_get_stdout('git', ['-C', publish_dir, 'submodule', 'update', '--init'], cmd_options);
          } else {
            cmd_get_stdout('git', ['clone', '--recursive', input_git_toplevel, publish_dir],
              ourbigbook.clone_and_set(cmd_options, 'dry_run', false));
          }

          // Set some variables especially for publishing.
          actual_input_dir = path.join(publish_dir, subdir_relpath);
          non_ourbigbook_options.ourbigbook_json_dir = actual_input_dir;
          publish_out_publish_dir = path.join(publish_tmpdir, 'publish');
          publish_out_publish_dist_dir = path.join(publish_out_publish_dir, ourbigbook_nodejs.DIST_BASENAME)
          non_ourbigbook_options.out_css_path = path.join(publish_out_publish_dist_dir, ourbigbook_nodejs.DIST_CSS_BASENAME);
          non_ourbigbook_options.out_js_path = path.join(publish_out_publish_dist_dir, ourbigbook_nodejs.DIST_JS_BASENAME);
          non_ourbigbook_options.external_css_and_js = true;
          // Remove all files from the gh-pages repository in case some were removed from the original source.
          if (
            fs.existsSync(path.join(publish_out_publish_dir, '.git'))
          ) {
            if (git_ls_files(publish_out_publish_dir).length > 0) {
              cmd_get_stdout('git', ['-C', publish_out_publish_dir, 'rm', '-r', '-f', '.'], cmd_options);
            }
          } else {
            fs.mkdirSync(publish_out_publish_dir, {recursive: true});
          }
        } else {
          actual_input_dir = inputPath;
          publish_out_publish_dir = outdir;
        }
        non_ourbigbook_options.outdir = publish_out_publish_dir;
        await create_db(ourbigbook_options, non_ourbigbook_options);
        await reconcile_db_and_filesystem(actual_input_dir, ourbigbook_options, non_ourbigbook_options);

        // Do the actual conversion.
        await convert_directory_extract_ids(actual_input_dir, ourbigbook_options, non_ourbigbook_options);
        if (!non_ourbigbook_options.had_error) {
          await check_db(non_ourbigbook_options)
        }
        if (commander.render) {
          if (!non_ourbigbook_options.had_error) {
            await convert_directory(actual_input_dir, ourbigbook_options, non_ourbigbook_options);
          }
        }

        // Generate redirects from ourbigbook.json.
        for (let [from, to] of ourbigbook_json.redirects) {
          if (ourbigbook_options.html_x_extension) {
            to +=  '.' + ourbigbook.HTML_EXT
          }
          generate_redirect_base(
            path.join(non_ourbigbook_options.outdir, from + '.' + ourbigbook.HTML_EXT),
            to
          )
        }

        // Publish the converted output if build succeeded.
        if (publish && !non_ourbigbook_options.had_error) {
          // Push the original source.
          if (!commander.dryRunPush) {
            cmd_get_stdout('git', ['-C', inputPath, 'push'], cmd_options);
          }
          cmd_get_stdout('git', ['-C', publish_out_publish_dir, 'init'], cmd_options);
          // https://stackoverflow.com/questions/42871542/how-to-create-a-git-repository-with-the-default-branch-name-other-than-master
          cmd_get_stdout('git', ['-C', publish_out_publish_dir, 'checkout', '-B', publish_branch], cmd_options);
          try {
            // Fails if remote already exists.
            cmd_get_stdout('git', ['-C', publish_out_publish_dir, 'remote', 'add', 'origin', remote_url], cmd_options);
          } catch(error) {
            cmd_get_stdout('git', ['-C', publish_out_publish_dir, 'remote', 'set-url', 'origin', remote_url], cmd_options);
          }
          // Ensure that we are up-to-date with the upstream gh-pages if one exists.
          cmd_get_stdout('git', ['-C', publish_out_publish_dir, 'fetch', 'origin'], cmd_options);
          cmd_get_stdout(
            'git',
            ['-C', publish_out_publish_dir, 'reset', `origin/${publish_branch}`],
            // Fails on the first commit in an empty repository.
            ourbigbook.clone_and_set(cmd_options, 'throw_on_error', false)
          );

          // Generate special files needed for GitHub pages.
          gemfile_content = "gem 'github-pages', group: :jekyll_plugins\n";
          fs.writeFileSync(path.join(publish_out_publish_dir, 'Gemfile'), gemfile_content);

          // Commit and push.
          if ('prepublish' in ourbigbook_json) {
            if (!commander.dryRun && !commander.dryRunPush && !commander.unsafeAce) {
              cli_error('prepublish in ourbigbook.json requires running with --unsafe-ace');
            }
            const prepublish_path = ourbigbook_json.prepublish
            if (!fs.existsSync(prepublish_path)) {
              cli_error(`${OURBIGBOOK_JSON_BASENAME} prepublish file not found: ${prepublish_path}`);
            }
            try {
              cmd_get_stdout(path.resolve(prepublish_path), [publish_out_publish_dir]);
            } catch(error) {
              cli_error(`${OURBIGBOOK_JSON_BASENAME} prepublish command exited non-zero, aborting`);
            }
          }

          // Copy runtime assets from dist/ into the output repository.
          const dir = fs.opendirSync(ourbigbook_nodejs.DIST_PATH)
          let dirent
          while ((dirent = dir.readSync()) !== null) {
            require('fs-extra').copySync(
              path.join(ourbigbook_nodejs.DIST_PATH, dirent.name),
              path.join(publish_out_publish_dist_dir, dirent.name)
            )
          }
          dir.closeSync()

          cmd_get_stdout('git', ['-C', publish_out_publish_dir, 'add', '.'], cmd_options);
          source_commit = git_sha(inputPath, src_branch);
          const args = ['-C', publish_out_publish_dir, 'commit', '-m', source_commit]
          if (git_has_commit(publish_out_publish_dir)) {
            args.push('--amend')
          }
          cmd_get_stdout('git', args, cmd_options);
          if (!commander.dryRunPush) {
            cmd_get_stdout('git', ['-C', publish_out_publish_dir, 'push', '-f', 'origin', `${publish_branch}:${publish_branch}`], cmd_options);
            // Mark the commit with the `published` branch to make it easier to find what was last published.
            cmd_get_stdout('git', ['-C', inputPath, 'checkout', '-B', 'published'], cmd_options);
            cmd_get_stdout('git', ['-C', inputPath, 'push', '-f', '--follow-tags'], cmd_options);
            cmd_get_stdout('git', ['-C', inputPath, 'checkout', '-'], cmd_options);
          }
        }
      }
    }
  }
  if (
    inputPath === undefined ||
    (output !== undefined && commander.stdout)
  ) {
    process.stdout.write(output);
  }
  perf_print('exit', ourbigbook_options)
  if (!commander.watch) {
    process.exit(non_ourbigbook_options.had_error);
  }
}
})().catch((e) => {
  console.error(e);
  process.exit(1);
})
